/*-------------------------------------------------------------------------
 *
 * prepare.c
 *	  通过 PREPARE、EXECUTE 和 DEALLOCATE 准备 SQL 语句
 *
 * 该模块还实现了通过扩展的 FE/BE 查询协议访问的准备语句的存储。
 *
 *
 * Copyright (c) 2002-2022, PostgreSQL Global Development Group
 *
 * IDENTIFICATION
 *	  src/backend/commands/prepare.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include <limits.h>

#include "access/xact.h"
#include "catalog/pg_type.h"
#include "commands/createas.h"
#include "commands/prepare.h"
#include "funcapi.h"
#include "miscadmin.h"
#include "nodes/nodeFuncs.h"
#include "parser/analyze.h"
#include "parser/parse_coerce.h"
#include "parser/parse_collate.h"
#include "parser/parse_expr.h"
#include "parser/parse_type.h"
#include "rewrite/rewriteHandler.h"
#include "tcop/pquery.h"
#include "tcop/utility.h"
#include "utils/builtins.h"
#include "utils/snapmgr.h"
#include "utils/timestamp.h"


/*
 * 存储准备好的查询的哈希表。这是每个后端的：查询计划不在后端之间共享。
 * 该哈希表的键是PREPARE和EXECUTE的参数（语句名称）；条目是PreparedStatement结构。
 */
static HTAB *prepared_queries = NULL;

static void fc_InitQueryHashTable(void);
static ParamListInfo fc_EvaluateParams(ParseState *fc_pstate,
									PreparedStatement *fc_pstmt, List *fc_params,
									EState *fc_estate);
static Datum fc_build_regtype_array(Oid *fc_param_types, int fc_num_params);

/*
 * 实现'PREPARE'工具语句。
 */
void PrepareQuery(ParseState *fc_pstate, PrepareStmt *fc_stmt,
			 int fc_stmt_location, int fc_stmt_len)
{
	RawStmt    *fc_rawstmt;
	CachedPlanSource *fc_plansource;
	Oid		   *fc_argtypes = NULL;
	int			fc_nargs;
	List	   *fc_query_list;

	/*
	 * 不允许空字符串语句名称（与协议级别的
	 * 无名语句冲突）。
	 */
	if (!fc_stmt->name || fc_stmt->name[0] == '\0')
		ereport(ERROR,
				(errcode(ERRCODE_INVALID_PSTATEMENT_DEFINITION),
				 errmsg("invalid statement name: must not be empty")));

	/*
	 * 需要将包含的语句包装在RawStmt节点中，以便将其传递给
	 * 解析分析。
	 */
	fc_rawstmt = makeNode(RawStmt);
	fc_rawstmt->stmt = fc_stmt->query;
	fc_rawstmt->stmt_location = fc_stmt_location;
	fc_rawstmt->stmt_len = fc_stmt_len;

	/*
	 * 在进行解析分析之前创建CachedPlanSource，因为它需要
	 * 查看未修改的原始解析树。
	 */
	fc_plansource = CreateCachedPlan(fc_rawstmt, fc_pstate->p_sourcetext,
								  CreateCommandTag(fc_stmt->query));

	/* 将TypeNames列表转换为类型OID数组 */
	fc_nargs = list_length(fc_stmt->argtypes);

	if (fc_nargs)
	{
		int			fc_i;
		ListCell   *fc_l;

		fc_argtypes = (Oid *) palloc(fc_nargs * sizeof(Oid));
		fc_i = 0;

		foreach(fc_l, fc_stmt->argtypes)
		{
			TypeName   *fc_tn = lfirst(fc_l);
			Oid			fc_toid = typenameTypeId(fc_pstate, fc_tn);

			fc_argtypes[fc_i++] = fc_toid;
		}
	}

	/*
	 * 使用这些参数类型分析语句（从上面传入的任何参数
	 * 将对它不可见），允许从上下文推断出未知参数的信息。
	 * 重写查询。结果可能是0、1或多个查询。
	 */
	fc_query_list = pg_analyze_and_rewrite_varparams(fc_rawstmt, fc_pstate->p_sourcetext,
												  &fc_argtypes, &fc_nargs, NULL);

	/* 完成填充CachedPlanSource */
	CompleteCachedPlan(fc_plansource,
					   fc_query_list,
					   NULL,
					   fc_argtypes,
					   fc_nargs,
					   NULL,
					   NULL,
					   CURSOR_OPT_PARALLEL_OK,	/* 允许并行模式 */
					   true);	/* 固定结果 */

	/*
	 * 保存结果。
	 */
	StorePreparedStatement(fc_stmt->name,
						   fc_plansource,
						   true);
}

/*
 * ExecuteQuery --- 实现'EXECUTE'工具语句。
 *
 * 此代码还支持CREATE TABLE ... AS EXECUTE。该情况通过传递非空的intoClause来表示。
 * DestReceiver已经为CREATE TABLE AS正确设置，但我们仍需在这里进行一些其他调整。
 */
void ExecuteQuery(ParseState *fc_pstate,
			 ExecuteStmt *fc_stmt, IntoClause *fc_intoClause,
			 ParamListInfo fc_params,
			 DestReceiver *fc_dest, QueryCompletion *fc_qc)
{
	PreparedStatement *fc_entry;
	CachedPlan *fc_cplan;
	List	   *fc_plan_list;
	ParamListInfo fc_paramLI = NULL;
	EState	   *fc_estate = NULL;
	Portal		fc_portal;
	char	   *fc_query_string;
	int			fc_eflags;
	long		fc_count;

	/* 在哈希表中查找它 */
	fc_entry = FetchPreparedStatement(fc_stmt->name, true);

	/* 不应该找到非固定结果的缓存计划 */
	if (!fc_entry->plansource->fixed_result)
		elog(ERROR, "EXECUTE does not support variable-result cached plans");

	/* 评估参数（如果有的话） */
	if (fc_entry->plansource->num_params > 0)
	{
		/*
		 * 需要一个EState来评估参数；在查询结束之前不得删除它，
		 * 以防参数是按引用传递的。请注意，传入的"params"
		 * 可能在参数表达式中被引用。
		 */
		fc_estate = CreateExecutorState();
		fc_estate->es_param_list_info = fc_params;
		fc_paramLI = fc_EvaluateParams(fc_pstate, fc_entry, fc_stmt->params, fc_estate);
	}

	/* 创建一个新的门户以运行查询 */
	fc_portal = CreateNewPortal();
	/* 不在pg_cursors中显示该门户，仅供内部使用 */
	fc_portal->visible = false;

	/* 将计划的保存查询字符串复制到门户的内存中 */
	fc_query_string = MemoryContextStrdup(fc_portal->portalContext,
									   fc_entry->plansource->query_string);

	/* 如有必要，重新规划，并为门户增加计划引用计数 */
	fc_cplan = GetCachedPlan(fc_entry->plansource, fc_paramLI, NULL, NULL);
	fc_plan_list = fc_cplan->stmt_list;

	/*
	 * 请勿在GetCachedPlan和PortalDefineQuery之间添加可能引发错误的任何逻辑，
	 * 否则会泄漏计划引用计数。
	 */
	PortalDefineQuery(fc_portal,
					  NULL,
					  fc_query_string,
					  fc_entry->plansource->commandTag,
					  fc_plan_list,
					  fc_cplan);

	/*
	 * 对于 CREATE TABLE ... AS EXECUTE，我们必须验证准备好的
	 * 语句是一个生成元组的语句。目前我们坚持它应该是一个普通的 SELECT。
	 * 在未来，我们可能考虑支持其他的东西，例如 INSERT ... RETURNING，
	 * 但是首先需要解决几个问题，尤其是如何处理这种情况下的 WITH NO DATA
	 * （我们真的想要抑制执行吗？）以及如何传递确定 OID 的 eflags
	 * （在这种情况下，PortalStart 不会处理它们，至于执行器是否会处理，
	 * 也不太清楚）。
	 *
	 * 对于 CREATE TABLE ... AS EXECUTE，我们还必须确保适当的
	 * eflags 和获取计数传递给 PortalStart/PortalRun。
	 */
	if (fc_intoClause)
	{
		PlannedStmt *fc_pstmt;

		if (list_length(fc_plan_list) != 1)
			ereport(ERROR,
					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
					 errmsg("prepared statement is not a SELECT")));
		fc_pstmt = linitial_node(PlannedStmt, fc_plan_list);
		if (fc_pstmt->commandType != CMD_SELECT)
			ereport(ERROR,
					(errcode(ERRCODE_WRONG_OBJECT_TYPE),
					 errmsg("prepared statement is not a SELECT")));

		/* 设置适当的 eflags */
		fc_eflags = GetIntoRelEFlags(fc_intoClause);

		/* 并告诉 PortalRun 是否运行到完成 */
		if (fc_intoClause->skipData)
			fc_count = 0;
		else
			fc_count = FETCH_ALL;
	}
	else
	{
		/* 普通的 EXECUTE */
		fc_eflags = 0;
		fc_count = FETCH_ALL;
	}

	/*
	 * 根据需要运行门户。
	 */
	PortalStart(fc_portal, fc_paramLI, fc_eflags, GetActiveSnapshot());

	(void) PortalRun(fc_portal, fc_count, false, true, fc_dest, fc_dest, fc_qc);

	PortalDrop(fc_portal, false);

	if (fc_estate)
		FreeExecutorState(fc_estate);

	/* 不需要释放其他内存，MemoryContext 将被重置 */
}

/*
 * EvaluateParams: 评估参数列表。
 *
 * pstate: 解析状态
 * pstmt: 我们正在获取参数的语句。
 * params: 给定参数表达式的列表（原始解析器输出！）
 * estate: 要使用的执行状态。
 *
 * 返回一个填充的 ParamListInfo -- 这可以稍后传递给
 * CreateQueryDesc()，允许执行器在查询执行期间利用参数。
 */
static ParamListInfo fc_EvaluateParams(ParseState *fc_pstate, PreparedStatement *fc_pstmt, List *fc_params,
			   EState *fc_estate)
{
	Oid		   *fc_param_types = fc_pstmt->plansource->param_types;
	int			fc_num_params = fc_pstmt->plansource->num_params;
	int			fc_nparams = list_length(fc_params);
	ParamListInfo fc_paramLI;
	List	   *fc_exprstates;
	ListCell   *fc_l;
	int			fc_i;

	if (fc_nparams != fc_num_params)
		ereport(ERROR,
				(errcode(ERRCODE_SYNTAX_ERROR),
				 errmsg("wrong number of parameters for prepared statement \"%s\"",
						fc_pstmt->stmt_name),
				 errdetail("Expected %d parameters but got %d.",
						   fc_num_params, fc_nparams)));

	/* 如果没有参数，快速退出 */
	if (fc_num_params == 0)
		return NULL;

	/*
	 * 我们必须对表达式进行解析分析。由于解析器
	 * 不会在其输入上随意涂写，因此首先复制。
	 */
	fc_params = copyObject(fc_params);

	fc_i = 0;
	foreach(fc_l, fc_params)
	{
		Node	   *fc_expr = lfirst(fc_l);
		Oid			fc_expected_type_id = fc_param_types[fc_i];
		Oid			fc_given_type_id;

		fc_expr = transformExpr(fc_pstate, fc_expr, EXPR_KIND_EXECUTE_PARAMETER);

		fc_given_type_id = exprType(fc_expr);

		fc_expr = coerce_to_target_type(fc_pstate, fc_expr, fc_given_type_id,
									 fc_expected_type_id, -1,
									 COERCION_ASSIGNMENT,
									 COERCE_IMPLICIT_CAST,
									 -1);

		if (fc_expr == NULL)
			ereport(ERROR,
					(errcode(ERRCODE_DATATYPE_MISMATCH),
					 errmsg("parameter $%d of type %s cannot be coerced to the expected type %s",
							fc_i + 1,
							format_type_be(fc_given_type_id),
							format_type_be(fc_expected_type_id)),
					 errhint("You will need to rewrite or cast the expression."),
					 parser_errposition(fc_pstate, exprLocation(lfirst(fc_l)))));

		/* 处理完成表达式中的排序规则。 */
		assign_expr_collations(fc_pstate, fc_expr);

		lfirst(fc_l) = fc_expr;
		fc_i++;
	}

	/* 准备表达式以便执行 */
	fc_exprstates = ExecPrepareExprList(fc_params, fc_estate);

	fc_paramLI = makeParamList(fc_num_params);

	fc_i = 0;
	foreach(fc_l, fc_exprstates)
	{
		ExprState  *fc_n = (ExprState *) lfirst(fc_l);
		ParamExternData *fc_prm = &fc_paramLI->params[fc_i];

		fc_prm->ptype = fc_param_types[fc_i];
		fc_prm->pflags = PARAM_FLAG_CONST;
		fc_prm->value = ExecEvalExprSwitchContext(fc_n,
											   GetPerTupleExprContext(fc_estate),
											   &fc_prm->isnull);

		fc_i++;
	}

	return fc_paramLI;
}


/*
 * 在首次使用时初始化查询哈希表。
 */
static void fc_InitQueryHashTable(void)
{
	HASHCTL		fc_hash_ctl;

	fc_hash_ctl.keysize = NAMEDATALEN;
	fc_hash_ctl.entrysize = sizeof(PreparedStatement);

	prepared_queries = hash_create("Prepared Queries",
								   32,
								   &fc_hash_ctl,
								   HASH_ELEM | HASH_STRINGS);
}

/*
 * 使用指定的键在哈希表中存储与查询相关的所有数据。
 * 传递的 CachedPlanSource 应该是“未保存”的
 * 以防我们在这里遇到错误；一旦我们创建了哈希
 * 表条目，就会保存它。
 */
void StorePreparedStatement(const char *fc_stmt_name,
					   CachedPlanSource *fc_plansource,
					   bool fc_from_sql)
{
	PreparedStatement *fc_entry;
	TimestampTz fc_cur_ts = GetCurrentStatementStartTimestamp();
	bool		fc_found;

	/* 如果必要，初始化哈希表 */
	if (!prepared_queries)
		fc_InitQueryHashTable();

	/* 向哈希表添加条目 */
	fc_entry = (PreparedStatement *) hash_search(prepared_queries,
											  fc_stmt_name,
											  HASH_ENTER,
											  &fc_found);

	/* 不应该有重复条目 */
	if (fc_found)
		ereport(ERROR,
				(errcode(ERRCODE_DUPLICATE_PSTATEMENT),
				 errmsg("prepared statement \"%s\" already exists",
						fc_stmt_name)));

	/* 填充哈希表条目 */
	fc_entry->plansource = fc_plansource;
	fc_entry->from_sql = fc_from_sql;
	fc_entry->prepare_time = fc_cur_ts;

	/* 现在可以安全地将 CachedPlanSource 移动到永久内存 */
	SaveCachedPlan(fc_plansource);
}

/*
 * 在哈希表中查找现有查询。如果查询实际上不存在，
 * 抛出 ereport(ERROR) 或根据第二个参数返回 NULL。
 *
 * 注意：这不会强制引用的 plancache 条目是有效的，
 * 因为并非所有调用者都关心。
 */
PreparedStatement *
FetchPreparedStatement(const char *fc_stmt_name, bool fc_throwError)
{
	PreparedStatement *fc_entry;

	/*
	 * 如果哈希表尚未初始化，它不能存储
	 * 任何内容，因此它不可能存储我们的计划。
	 */
	if (prepared_queries)
		fc_entry = (PreparedStatement *) hash_search(prepared_queries,
												  fc_stmt_name,
												  HASH_FIND,
												  NULL);
	else
		fc_entry = NULL;

	if (!fc_entry && fc_throwError)
		ereport(ERROR,
				(errcode(ERRCODE_UNDEFINED_PSTATEMENT),
				 errmsg("prepared statement \"%s\" does not exist",
						fc_stmt_name)));

	return fc_entry;
}

/*
 * 给定一个准备好的语句，确定它将产生的结果 tupledesc。
 * 如果执行不会返回元组，则返回 NULL。
 *
 * 注意：结果会创建或复制到当前内存上下文中。
 */
TupleDesc FetchPreparedStatementResultDesc(PreparedStatement *fc_stmt)
{
	/*
	 * 由于我们不允许预编译语句的结果元组描述符更改，
	 * 因此这里不需要担心重新验证缓存计划。
	 */
	Assert(fc_stmt->plansource->fixed_result);
	if (fc_stmt->plansource->resultDesc)
		return CreateTupleDescCopy(fc_stmt->plansource->resultDesc);
	else
		return NULL;
}

/*
 * 给定一个返回元组的预编译语句，提取查询
 * 目标列表。如果语句没有可确定的
 * 目标列表，则返回NIL。
 *
 * 注意：这有点难看，但由于它仅在像
 * EXECUTE命令上的Describe Statement这样的特殊情况下使用，
 * 我们对此的效率不太担心。
 */
List * FetchPreparedStatementTargetList(PreparedStatement *fc_stmt)
{
	List	   *fc_tlist;

	/* 获取计划的主要目标列表 */
	fc_tlist = CachedPlanGetTargetList(fc_stmt->plansource, NULL);

	/* 复制到调用者的上下文，以防计划失效 */
	return copyObject(fc_tlist);
}

/*
 * 实现“DEALLOCATE”实用语句：从存储中删除
 * 指定的计划。
 */
void DeallocateQuery(DeallocateStmt *fc_stmt)
{
	if (fc_stmt->name)
		DropPreparedStatement(fc_stmt->name, true);
	else
		DropAllPreparedStatements();
}

/*
 * DEALLOCATE的内部版本
 *
 * 如果showError为false，删除不存在的语句是无操作。
 */
void DropPreparedStatement(const char *fc_stmt_name, bool fc_showError)
{
	PreparedStatement *fc_entry;

	/* 查找查询的哈希表条目；如有需要则引发错误 */
	fc_entry = FetchPreparedStatement(fc_stmt_name, fc_showError);

	if (fc_entry)
	{
		/* 释放计划缓存条目 */
		DropCachedPlan(fc_entry->plansource);

		/* 现在我们可以删除哈希表条目 */
		hash_search(prepared_queries, fc_entry->stmt_name, HASH_REMOVE, NULL);
	}
}

/*
 * 删除所有缓存的语句。
 */
void DropAllPreparedStatements(void)
{
	HASH_SEQ_STATUS fc_seq;
	PreparedStatement *fc_entry;

	/* 没有缓存 */
	if (!prepared_queries)
		return;

	/* 遍历缓存 */
	hash_seq_init(&fc_seq, prepared_queries);
	while ((fc_entry = hash_seq_search(&fc_seq)) != NULL)
	{
		/* 释放计划缓存条目 */
		DropCachedPlan(fc_entry->plansource);

		/* 现在我们可以删除哈希表条目 */
		hash_search(prepared_queries, fc_entry->stmt_name, HASH_REMOVE, NULL);
	}
}

/*
 * 实现“EXPLAIN EXECUTE”实用语句。
 *
 * “into”为NULL，除非我们正在执行EXPLAIN CREATE TABLE AS EXECUTE，
 * 在这种情况下，执行查询应导致创建该表。
 *
 * 注意：传入的queryString是EXPLAIN EXECUTE的，
 * 而不是原始的PREPARE；我们从计划缓存中获取后者字符串。
 */
void ExplainExecuteQuery(ExecuteStmt *fc_execstmt, IntoClause *fc_into, ExplainState *fc_es,
					const char *fc_queryString, ParamListInfo fc_params,
					QueryEnvironment *fc_queryEnv)
{
	PreparedStatement *fc_entry;
	const char *fc_query_string;
	CachedPlan *fc_cplan;
	List	   *fc_plan_list;
	ListCell   *fc_p;
	ParamListInfo fc_paramLI = NULL;
	EState	   *fc_estate = NULL;
	instr_time	fc_planstart;
	instr_time	fc_planduration;
	BufferUsage fc_bufusage_start,
				fc_bufusage;

	if (fc_es->buffers)
		fc_bufusage_start = pgBufferUsage;
	INSTR_TIME_SET_CURRENT(fc_planstart);

	/* 在哈希表中查找它 */
	fc_entry = FetchPreparedStatement(fc_execstmt->name, true);

	/* 不应该找到非固定结果的缓存计划 */
	if (!fc_entry->plansource->fixed_result)
		elog(ERROR, "EXPLAIN EXECUTE does not support variable-result cached plans");

	fc_query_string = fc_entry->plansource->query_string;

	/* 评估参数（如果有的话） */
	if (fc_entry->plansource->num_params)
	{
		ParseState *fc_pstate;

		fc_pstate = make_parsestate(NULL);
		fc_pstate->p_sourcetext = fc_queryString;

		/*
		 * 需要一个EState来评估参数；在查询结束之前不得删除它，
		 * 以防参数是按引用传递的。请注意，传入的"params"
		 * 可能在参数表达式中被引用。
		 */
		fc_estate = CreateExecutorState();
		fc_estate->es_param_list_info = fc_params;

		fc_paramLI = fc_EvaluateParams(fc_pstate, fc_entry, fc_execstmt->params, fc_estate);
	}

	/* 如有必要，重新规划，并获取瞬时引用计数 */
	fc_cplan = GetCachedPlan(fc_entry->plansource, fc_paramLI,
						  CurrentResourceOwner, fc_queryEnv);

	INSTR_TIME_SET_CURRENT(fc_planduration);
	INSTR_TIME_SUBTRACT(fc_planduration, fc_planstart);

	/* 计算缓冲计数器的差异。 */
	if (fc_es->buffers)
	{
		memset(&fc_bufusage, 0, sizeof(BufferUsage));
		BufferUsageAccumDiff(&fc_bufusage, &pgBufferUsage, &fc_bufusage_start);
	}

	fc_plan_list = fc_cplan->stmt_list;

	/* 解释每个查询 */
	foreach(fc_p, fc_plan_list)
	{
		PlannedStmt *fc_pstmt = lfirst_node(PlannedStmt, fc_p);

		if (fc_pstmt->commandType != CMD_UTILITY)
			ExplainOnePlan(fc_pstmt, fc_into, fc_es, fc_query_string, fc_paramLI, fc_queryEnv,
						   &fc_planduration, (fc_es->buffers ? &fc_bufusage : NULL));
		else
			ExplainOneUtility(fc_pstmt->utilityStmt, fc_into, fc_es, fc_query_string,
							  fc_paramLI, fc_queryEnv);

		/* 无需CommandCounterIncrement，因为ExplainOnePlan已完成此操作 */

		/* 用适当的分隔符分隔计划 */
		if (lnext(fc_plan_list, fc_p) != NULL)
			ExplainSeparatePlans(fc_es);
	}

	if (fc_estate)
		FreeExecutorState(fc_estate);

	ReleaseCachedPlan(fc_cplan, CurrentResourceOwner);
}

/*
 * 这个返回集合的函数读取所有预编译语句并
 * 返回一个集合（名称，语句，准备时间，参数类型，来自SQL，
 * 通用计划，自定义计划）。
 */
Datum pg_prepared_statement(PG_FUNCTION_ARGS)
{
	ReturnSetInfo *fc_rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;

	/*
	 * 我们将所有元组放入一个元组存储中，通过一次扫描哈希表。
	 * 这避免了在调用之间哈希表可能发生变化的问题。
	 */
	InitMaterializedSRF(fcinfo, 0);

	/* 哈希表可能未初始化 */
	if (prepared_queries)
	{
		HASH_SEQ_STATUS fc_hash_seq;
		PreparedStatement *fc_prep_stmt;

		hash_seq_init(&fc_hash_seq, prepared_queries);
		while ((fc_prep_stmt = hash_seq_search(&fc_hash_seq)) != NULL)
		{
			Datum		fc_values[7];
			bool		fc_nulls[7];

			MemSet(fc_nulls, 0, sizeof(fc_nulls));

			fc_values[0] = CStringGetTextDatum(fc_prep_stmt->stmt_name);
			fc_values[1] = CStringGetTextDatum(fc_prep_stmt->plansource->query_string);
			fc_values[2] = TimestampTzGetDatum(fc_prep_stmt->prepare_time);
			fc_values[3] = fc_build_regtype_array(fc_prep_stmt->plansource->param_types,
											fc_prep_stmt->plansource->num_params);
			fc_values[4] = BoolGetDatum(fc_prep_stmt->from_sql);
			fc_values[5] = Int64GetDatumFast(fc_prep_stmt->plansource->num_generic_plans);
			fc_values[6] = Int64GetDatumFast(fc_prep_stmt->plansource->num_custom_plans);

			tuplestore_putvalues(fc_rsinfo->setResult, fc_rsinfo->setDesc,
								 fc_values, fc_nulls);
		}
	}

	return (Datum) 0;
}

/*
 * 这个实用函数接受一个Oids的C数组，并返回一个Datum
 * 指向一维Postgres regtypes数组。空
 * 数组作为零元素数组返回，不是NULL。
 */
static Datum fc_build_regtype_array(Oid *fc_param_types, int fc_num_params)
{
	Datum	   *fc_tmp_ary;
	ArrayType  *fc_result;
	int			fc_i;

	fc_tmp_ary = (Datum *) palloc(fc_num_params * sizeof(Datum));

	for (fc_i = 0; fc_i < fc_num_params; fc_i++)
		fc_tmp_ary[fc_i] = ObjectIdGetDatum(fc_param_types[fc_i]);

	/* XXX：这硬编码了关于regtype类型的假设 */
	fc_result = construct_array(fc_tmp_ary, fc_num_params, REGTYPEOID,
							 4, true, TYPALIGN_INT);
	return PointerGetDatum(fc_result);
}
